package com.iceberg.adgt.io;

import com.iceberg.adgt.domain.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.util.Map;
import java.util.Set;

public final class XMLWriter implements Writer {

	private static final Logger LOG = LoggerFactory.getLogger(XMLReader.class);
	private static final String NAME = "name";
	private static final String VALUE = "value";
	private static final String AUTHOR = "author";
	private static final String LAYER = "layer";
	private DomainFacade facade;
	private ArchitectureDTO architecture;
	private StructureDTO structure;
	private Document dom;

	public XMLWriter() {
		facade = DomainFacadeImpl.getInstance();
	}

	@Override
	public void writeArchitectureFile(File file) {
		architecture = facade.getArchitecture();
		createArchitectureDOM();
		transform(file);
	}

	@Override
	public void writeStructureFile(File file) {
		architecture = facade.getArchitecture();
		structure = facade.getStructure();
		createStructureDOM();
		transform(file);
	}

	private void createStructureDOM() {
		createDom();

		Element root = dom.createElement("structureDefinition");
		dom.appendChild(root);

		addStructureName();
		addStructureAuthor();
		addStructureLayers();
		addStructureUnits();
	}

	private void createArchitectureDOM() {
		createDom();

		Element root = dom.createElement("architectureDefinition");
		dom.appendChild(root);

		addArchitectureName();
		addArchitectureAuthor();
		addArchitectureLayers();
		addArchitectureAllowedLayers();
	}

	private void createDom() {
		try {
			DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
			dom = documentBuilder.newDocument();
		} catch (ParserConfigurationException ex) {
			LOG.error("Invalid xml", ex);
		}
	}

	private void transform(File file) {
		Source source = new DOMSource(dom);

		Result result = new StreamResult(file);
		try {
			Transformer xformer = TransformerFactory.newInstance().newTransformer();
			xformer.setOutputProperty(OutputKeys.INDENT, "yes");
			xformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
			try {
				xformer.transform(source, result);
			} catch (TransformerException ex) {
				LOG.error("Invalid transform", ex);
			}
		} catch (TransformerConfigurationException ex) {
			LOG.error("Invalid xml", ex);
		}
	}

	private void addArchitectureName() {
		Element root = (Element) dom.getFirstChild();

		if (root != null) {
			Element element = dom.createElement(NAME);
			element.setAttribute(VALUE, architecture.getName());
			root.appendChild(element);
		}
	}

	private void addArchitectureAuthor() {
		Element root = (Element) dom.getFirstChild();

		if (root != null) {
			Element element = dom.createElement(AUTHOR);
			element.setAttribute(VALUE, architecture.getAuthor());
			root.appendChild(element);
		}
	}

	private void addStructureName() {
		Element root = (Element) dom.getFirstChild();

		if (root != null) {
			Element element = dom.createElement(NAME);
			element.setAttribute(VALUE, structure.getName());
			root.appendChild(element);
		}
	}

	private void addStructureAuthor() {
		Element root = (Element) dom.getFirstChild();

		if (root != null) {
			Element element = dom.createElement(AUTHOR);
			element.setAttribute(VALUE, structure.getAuthor());
			root.appendChild(element);
		}
	}

	private void addArchitectureAllowedLayers() {
		NodeList layerElements = dom.getElementsByTagName(LAYER);

		for (int i = 0; i < layerElements.getLength(); i++) {
			Node layerElement = layerElements.item(i);
			Map<String, LayerDTO> allowedLayersForLayer = architecture.getLayers().get(layerElement.getAttributes().getNamedItem(NAME).getNodeValue()).getAllowedLayers();

			if (allowedLayersForLayer.values().size() > 0) {
				Element allowedLayerElement = dom.createElement("allowedToAccess");

				for (LayerDTO layer : allowedLayersForLayer.values()) {
					Element accessLayer = dom.createElement("accessLayer");
					accessLayer.setAttribute(NAME, layer.getName());
					allowedLayerElement.appendChild(accessLayer);
				}
				layerElement.appendChild(allowedLayerElement);
			}
		}
	}

	private void addArchitectureLayers() {
		Map<String, LayerDTO> layers = architecture.getLayers();
		Element root = (Element) dom.getFirstChild();

		if (root != null) {
			for (LayerDTO layer : layers.values()) {
				Element layerElement = dom.createElement(LAYER);
				layerElement.setAttribute(NAME, layer.getName());
				layerElement.setAttribute("number", Integer.toString(layer.getNumber()));

				root.appendChild(layerElement);
			}
		}
	}

	private void addStructureUnits() {
		NodeList layerElements = dom.getElementsByTagName(LAYER);

		for (int i = 0; i < layerElements.getLength(); i++) {
			Node layerElement = layerElements.item(i);
			Set<UnitDTO> units = architecture.getLayers().get(layerElement.getAttributes().getNamedItem(NAME).getNodeValue()).getUnits();

			for (UnitDTO unit : units) {
				if (unit.getType().getName().equals("PACKAGE")) {
					Element packageElement = dom.createElement("package");
					packageElement.setAttribute(NAME, unit.getName());
					layerElement.appendChild(packageElement);
				} else if (unit.getType().getName().equals("CLASS")) {
					Element classElement = dom.createElement("class");
					classElement.setAttribute(NAME, unit.getName());
					layerElement.appendChild(classElement);
				}
			}
		}
	}

	private void addStructureLayers() {
		Map<String, LayerDTO> layerMap = architecture.getLayers();
		Element root = (Element) dom.getFirstChild();

		if (root != null) {
			for (LayerDTO layer : layerMap.values()) {
				Element layerElement = dom.createElement(LAYER);
				layerElement.setAttribute(NAME, layer.getName());
				root.appendChild(layerElement);
			}
		}
	}
}